P01-2. 서비스 메시의 탄생과 Envoy xDS - 실습 검증
Part 01 (실습): L4 Keep-Alive 쏠림 재현부터 Istio 설치 · Envoy xDS 5단계 해부까지
- 서비스 메시가 없는 기본 쿠버네티스 환경에서 HTTP Keep-Alive 요청이 한쪽 파드로만 100% 쏠리는 현상(L4 Connection Pinning) 을 직접 재현한다.
- Istio 1.31.3 컨트롤 플레인(
istiod)을 설치하고, 네임스페이스에 라벨 하나(istio-injection=enabled)를 붙여 파드 옆에 Envoy 사이드카(2/2) 를 자동으로 끼워 넣는다. - 사이드카가 붙은 뒤 똑같은 Keep-Alive 요청을 보냈을 때, Envoy(L7)가 요청을 한 장씩 뜯어 두 파드로 골고루 나눠 주는 것을 눈으로 확인한다.
istioctl proxy-config명령어로 Envoy 메모리 속의xDS5단계(LDS→RDS→CDS→EDS+SDS) 를 하나씩 직접 열어보고 읽는 법을 익힌다.
- 00. 사전 준비에서 설정한 Docker Desktop 내장 3노드 클러스터(
docker-desktop)가 켜져 있어야 합니다. - Windows PowerShell 창을 열고 아래 명령어로 작업 폴더 이동과 클러스터 상태를 먼저 확인합니다.
$env:Path += ";$HOME\bin" cd "$HOME\mesh-study" kubectl config current-context kubectl get nodesdocker-desktop과 노드 3대(desktop-control-plane,desktop-worker,desktop-worker2)가 모두Ready로 보이면 바로 시작합니다.
| 단계 | 검증 내용 |
|---|---|
| 1 | 메시 도입 전: 샘플 앱 배포와 기본 구조(1/1) 확인 |
| 2 | 메시 도입 전: 기본 kube-proxy(L4)의 Keep-Alive 한쪽 쏠림 재현 |
| 3 | Istio 1.31.3 설치 및 사이드카(2/2) 자동 주입 |
| 4 | 메시 도입 후: Envoy(L7)의 Keep-Alive 요청 단위 분산 실측 |
| 5 | Envoy 내부 메모리 해부: xDS 5단계(LDS·RDS·CDS·EDS·SDS) 직접 열어보기 |
| 6 | 상태 유지 및 Part 02 연계 준비 |
실습 1. 메시 도입 전: 샘플 앱 배포와 기본 구조(1/1) 확인
먼저 서비스 메시(Istio)가 전혀 없는 순수 쿠버네티스 상태에서 주문 서버(order-api 파드 2개)와 요청을 보낼 클라이언트(study-client 파드 1개)를 띄워 보겠습니다.
1-1. 네임스페이스 및 order-api.yaml 만들기
PowerShell 창에 아래 명령어를 복사해 붙여넣어 part1-demo 네임스페이스를 만들고 order-api.yaml 파일을 저장합니다.
kubectl create namespace part1-demo
@"
apiVersion: apps/v1
kind: Deployment
metadata:
name: order-api
namespace: part1-demo
spec:
replicas: 2
selector:
matchLabels:
app: order-api
template:
metadata:
labels:
app: order-api
version: v1
spec:
containers:
- name: echo
image: mendhak/http-https-echo:35
ports:
- containerPort: 8080
env:
- name: HTTP_PORT
value: "8080"
---
apiVersion: v1
kind: Service
metadata:
name: order-api
namespace: part1-demo
spec:
selector:
app: order-api
ports:
- name: http
port: 80
targetPort: 8080
---
apiVersion: v1
kind: Pod
metadata:
name: study-client
namespace: part1-demo
labels:
app: study-client
spec:
containers:
- name: study-client
image: curlimages/curl:8.10.1
command: ["sleep", "3650d"]
"@ | Set-Content -Path order-api.yaml -Encoding utf8
mendhak/http-https-echo:35(order-api역할): 요청을 보내면 "지금 어느 파드(hostname)가 이 요청을 받았는지" 와 요청 헤더 정보를 깔끔한 JSON으로 그대로 돌려주는 메아리(Echo) 테스트용 웹 서버입니다. 로드밸런싱과 프록시 헤더를 눈으로 확인하기에 가장 좋은 이미지입니다.curlimages/curl:8.10.1(study-client역할): 클러스터 안에서curl명령어로 HTTP 요청을 쏘기 위한 클라이언트 파드입니다. 꺼지지 않게sleep 3650d(10년 동안 대기) 명령을 걸어 두었습니다.- Service의
ports.name: http에 주목하세요!: Istio 같은 서비스 메시는 Service의 포트 이름(name: http,name: grpc,name: tcp-postgres)을 보고 "아, 이 포트는 HTTP(L7) 통신이구나!" 라고 프로토콜을 자동으로 판별합니다. 실무에서도 Service 포트에name: http처럼 프로토콜 이름을 명시하는 습관이 매우 중요합니다.
1-2. 클러스터에 배포하고 상태 확인하기
kubectl apply -f order-api.yaml
kubectl wait --for=condition=Ready pod -l app=order-api -n part1-demo --timeout=120s
kubectl wait --for=condition=Ready pod/study-client -n part1-demo --timeout=120s
kubectl get pods -n part1-demo -o wide
출력 예시:
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
order-api-6545864b4b-688ch 1/1 Running 0 22s 10.244.2.2 desktop-worker <none> <none>
order-api-6545864b4b-pb2nh 1/1 Running 0 22s 10.244.1.2 desktop-worker2 <none> <none>
study-client 1/1 Running 0 22s 10.244.1.3 desktop-worker2 <none> <none>
READY 1/1: 지금 각 파드 안에는 컨테이너가 딱 1개씩(내 앱만) 들어 있습니다. 아직 사이드카 비서(Envoy)가 붙기 전입니다.NODE열:order-api파드 2개가 각각desktop-worker(10.244.2.2)와desktop-worker2(10.244.1.2) 두 대의 일꾼 서버에 사이좋게 하나씩 나뉘어 배치되었습니다.
1-3. 쿠버네티스 Service 와 EndpointSlice 확인
kubectl get svc,endpointslices -n part1-demo
출력 예시:
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/order-api ClusterIP 10.96.236.120 <none> 80/TCP 35s
NAME ADDRESSTYPE PORTS ENDPOINTS AGE
endpointslice.discovery.k8s.io/order-api-tscng IPv4 8080 10.244.2.2,10.244.1.2 35s
order-apiService는 고정된 대표 IP(10.96.236.120:80)를 받았고, 그 뒤에는 실제 파드 2개의 IP(10.244.2.2:8080,10.244.1.2:8080)가 연결되어 있습니다.
실습 2. 메시 도입 전: 기본 kube-proxy(L4)의 Keep-Alive 한쪽 쏠림 재현
이론 개념 문서(P01-1) 3절에서 살펴본 "기본 kube-proxy(L4)는 전화 연결(TCP)을 끊지 않고 재사용(Keep-Alive)하면 무조건 한쪽 파드로만 요청을 몰아준다" 는 사실을 직접 실험해 보겠습니다.
2-1. 실험 A: 매번 전화를 끊고 새로 걸 때 (curl을 4번 따로 실행)
먼저 for 반복문으로 curl 프로그램을 4번 따로따로 실행해 봅니다. 프로그램이 4번 새로 실행되므로 TCP 전화 연결(SYN)도 4번 새로 걸립니다.
kubectl exec -n part1-demo study-client -- sh -c 'for i in 1 2 3 4; do curl -s http://order-api | grep -o "\"hostname\": \"order-api[^\"]*\""; done'
출력 예시: 매번 새 TCP 연결을 맺으므로 kube-proxy가 두 파드로 나름대로 섞어서 보냅니다.
"hostname": "order-api-6545864b4b-688ch"
"hostname": "order-api-6545864b4b-pb2nh"
"hostname": "order-api-6545864b4b-688ch"
"hostname": "order-api-6545864b4b-pb2nh"
2-2. 실험 B: 실제 운영 환경처럼 Keep-Alive로 연결을 유지한 채 4번 연속 호출할 때
하지만 실제 운영 서버(Java, Python, Go, Node.js, gRPC)는 요청마다 연결을 끊지 않고 HTTP Keep-Alive 로 단 1개의 TCP 연결을 유지한 채 여러 요청을 보냅니다.
curl에서 주소 뒤에 ?[1-4] 를 붙이면, curl을 딱 한 번만 켜서 TCP 전화선을 1개만 연결한 뒤 그 전화선 위로 1번, 2번, 3번, 4번 HTTP 요청을 연속으로 보냅니다. 실행해 볼까요?
kubectl exec -n part1-demo study-client -- curl -s --http1.1 "http://order-api/test?[1-4]" | Select-String '"hostname"'
-s(silent): 다운로드 진행률 표 같은 불필요한 출력을 숨기고 응답 본문(JSON)만 깔끔하게 보여줍니다.--http1.1: HTTP/1.1 표준의 기본 동작인 Keep-Alive(연결 재사용) 를 사용하겠다고 명시합니다."http://order-api/test?[1-4]":curl의 범위 지정(Globbing) 기능입니다./test?1,/test?2,/test?3,/test?4네 번의 HTTP 요청을 처음 맺은 똑같은 TCP 연결 하나를 재사용해서 연달아 쏩니다.Select-String '"hostname"': PowerShell에서 긴 JSON 결과 중"hostname"이 적힌 줄만 골라내는 필터입니다(리눅스의grep과 같습니다).
출력 예시 (실측 결과):
"hostname": "order-api",
"hostname": "order-api-6545864b4b-pb2nh"
"hostname": "order-api",
"hostname": "order-api-6545864b4b-pb2nh"
"hostname": "order-api",
"hostname": "order-api-6545864b4b-pb2nh"
"hostname": "order-api",
"hostname": "order-api-6545864b4b-pb2nh"
pb2nh 파드 하나로만 몰렸습니다!
분명히 파드는 688ch와 pb2nh 2개가 멀쩡히 살아 있는데, Keep-Alive로 연결을 재사용하자마자 4번의 요청이 100% pb2nh 파드 한 곳으로만 쏠렸습니다! (688ch 파드는 요청을 단 1건도 받지 못하고 놀고 있습니다.)
- 왜 그럴까요? 각 줄의 위쪽
"hostname": "order-api"는 내가 요청한 도메인 이름(Host헤더)이고, 아래쪽 들여쓰기된"hostname": "order-api-6545864b4b-pb2nh"가 실제로 응답한 파드의 이름입니다. - 첫 번째 요청(
/test?1)이 출발할 때kube-proxy(L4)가pb2nh파드로 TCP 전화선을 연결해 주었고, 그 뒤/test?2,/test?3,/test?4는 같은 전화선을 타고 갔기 때문에kube-proxy가 전혀 개입하지 못하고 전부pb2nh로만 들어간 것입니다(L4 커넥션 피닝).
실습 3. Istio 1.31.3 설치 및 사이드카(2/2) 자동 주입
이제 우리 클러스터에 서비스 메시인 Istio 1.31.3을 설치하고, 파드 옆에 똑똑한 L7 비서(Envoy 사이드카)를 붙여 보겠습니다.
3-1. istioctl 로 Istio 컨트롤 플레인(istiod) 설치하기
Part 01~02에서는 1세대 사이드카 구조를 배우기 위해 기본 프로필(--set profile=default)로 설치합니다.
istioctl install --set profile=default -y
출력 예시: 약 30~60초 정도 지나면 아래와 같이 설치 완료 메시지가 나옵니다.
✔ Istio core installed ⛵️
✔ Istiod installed 🧠
✔ Ingress gateways installed 🛫
✔ Installation complete
설치된 Istio 구성 요소들을 확인해 봅니다.
kubectl get pods,svc -n istio-system
출력 예시:
NAME READY STATUS RESTARTS AGE
pod/istio-ingressgateway-5b57c8df78-w27tk 1/1 Running 0 27s
pod/istiod-6f5989b555-tkf9q 1/1 Running 0 37s
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/istio-ingressgateway LoadBalancer 10.96.161.224 <pending> 15021:31954/TCP,80:30187/TCP,443:31626/TCP 27s
service/istiod ClusterIP 10.96.122.143 <none> 15010/TCP,15012/TCP,443/TCP,15014/TCP 37s
istiod의 포트 번호 의미
service/istiod의15012/TCP포트가 바로 현장의 Envoy 사이드카들과 암호화된 직통 전화(gRPC 스트리밍)를 연결해xDS작전 지시와 인증서를 내려보내는 포트입니다!443/TCP포트는 새로운 파드가 만들어질 때 자동으로 Envoy 사이드카를 끼워 넣어 주는 웹훅(Webhook) 포트입니다(Part 02에서 상세히 해부합니다).
3-2. 네임스페이스에 사이드카 자동 주입 스티커(Label) 붙이기
이제 쿠버네티스에게 "앞으로 part1-demo라는 부서(네임스페이스)에 새로 만들어지는 모든 파드에는 자동으로 Envoy 사이드카 비서를 한 명씩 붙여 줘!" 라고 이름표(Label)를 붙여 줍니다.
kubectl label namespace part1-demo istio-injection=enabled --overwrite
kubectl get namespace part1-demo --show-labels
출력 예시: LABELS 칸에 istio-injection=enabled 가 붙은 것이 보입니다.
NAME STATUS AGE LABELS
part1-demo Active 5m istio-injection=enabled,kubernetes.io/metadata.name=part1-demo
3-3. 파드들을 재시작해서 사이드카(2/2) 끼워 넣기
네임스페이스에 istio-injection=enabled 라벨을 붙였다고 해서, 이미 떠 있는 기존 파드들에 갑자기 사이드카가 뿅 하고 들어가지는 않습니다!
사이드카 주입은 "파드가 처음 생성되는 순간" 에만 일어납니다. 따라서 이미 떠 있는 파드들은 한 번 재시작(rollout restart 또는 재생성)해 주어야 새 파드가 만들어지면서 사이드카가 함께 들어갑니다.
아래 명령어로 order-api와 study-client를 다시 띄웁니다.
# 1. order-api 디플로이먼트 롤링 재시작
kubectl rollout restart deployment/order-api -n part1-demo
kubectl rollout status deployment/order-api -n part1-demo
# 2. study-client 파드 지우고 다시 만들기
kubectl delete pod study-client -n part1-demo
kubectl apply -f order-api.yaml
kubectl wait --for=condition=Ready pod/study-client -n part1-demo --timeout=120s
이제 파드 목록을 다시 조회해 보세요!
kubectl get pods -n part1-demo -o wide
출력 예시:
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
order-api-d757f5dc9-fpz54 2/2 Running 0 25s 10.244.1.4 desktop-worker2 <none> <none>
order-api-d757f5dc9-hsthv 2/2 Running 0 23s 10.244.3.5 desktop-worker <none> <none>
study-client 2/2 Running 0 15s 10.244.1.5 desktop-worker2 <none> <none>
READY 칸이 1/1 에서 2/2 로 바뀌었습니다!
우리는 order-api.yaml 파일을 단 한 글자도 수정하지 않았습니다!
그런데도 READY가 **2/2**가 되었습니다. 진짜로 파드 안에 어떤 컨테이너 2개가 들어 있는지 확인해 볼까요?
kubectl get pod study-client -n part1-demo -o jsonpath="{.spec.containers[*].name}`n"
출력 예시:
study-client istio-proxy
원래 우리가 적은 study-client 컨테이너 옆에, istio-proxy (Envoy 사이드카 프록시) 컨테이너가 나란히 들어앉았습니다!
실습 4. 메시 도입 후: Envoy(L7)의 Keep-Alive 요청 단위 분산 실측
자, 이제 대망의 비교 실험입니다! 실습 2-2에서 쳤던 것과 토씨 하나 다르지 않은 똑같은 HTTP Keep-Alive 연속 4회 요청 명령어를 그대로 다시 실행해 보겠습니다.
study-client, istio-proxy)이므로 -c study-client 옵션을 붙여 어느 컨테이너에서 명령을 실행할지 알려줍니다.kubectl exec -n part1-demo study-client -c study-client -- curl -s --http1.1 "http://order-api/test?[1-4]" | Select-String '"hostname"'
출력 예시 (실측 결과):
"hostname": "order-api",
"hostname": "order-api-d757f5dc9-fpz54"
"hostname": "order-api",
"hostname": "order-api-d757f5dc9-hsthv"
"hostname": "order-api",
"hostname": "order-api-d757f5dc9-fpz54"
"hostname": "order-api",
"hostname": "order-api-d757f5dc9-hsthv"
fpz54 → hsthv → fpz54 → hsthv) 성공!
- 실습 2-2에서는 똑같은 Keep-Alive 요청 4번이 전부 한 파드(
pb2nh)로만 몰렸었습니다. - 그런데 Envoy 사이드카(
2/2)가 붙자마자, 내curl프로그램은 여전히 단 1개의 TCP 연결(Keep-Alive) 만 유지하고 있는데도 옆자리의 Envoy 프록시가 L7(HTTP 요청) 단위로 한 장씩 뜯어서fpz54파드와hsthv파드로 번갈아 가며(Round-Robin) 배달해 주었습니다!
Bonus로, 지금 order-api 파드가 받은 요청 헤더 안에 Envoy가 어떤 흔적을 남겼는지 1번만 전체 JSON을 출력해 볼까요?
kubectl exec -n part1-demo study-client -c study-client -- curl -s http://order-api/headers
출력 예시 (일부):
{
"path": "/headers",
"headers": {
"host": "order-api",
"user-agent": "curl/8.10.1",
"x-forwarded-proto": "http",
"x-request-id": "49f1a6e3-82b1-94c2-b10a-72e19c08a312",
"x-envoy-attempt-count": "1",
"x-b3-traceid": "8c14a92fb71e0420a19c7b381920bc14",
"x-b3-spanid": "a19c7b381920bc14",
"x-b3-sampled": "0",
"x-forwarded-client-cert": "By=spiffe://cluster.local/ns/part1-demo/sa/default;Hash=...;Subject=\"\";URI=spiffe://cluster.local/ns/part1-demo/sa/default"
},
"hostname": "order-api-d757f5dc9-fpz54"
}
내 curl은 아무 헤더도 안 보냈는데, Envoy 비서끼리 통신하면서 자동으로 다음 정보들을 붙여 주었습니다:
x-envoy-attempt-count: "1": "이 요청은 첫 번째 시도(1회차)입니다" (재시도가 일어나면2,3으로 올라갑니다).x-request-id/x-b3-traceid: 요청마다 고유한 택배 송장 번호(Trace ID)를 자동으로 발급해 분산 추적(Tracing)이 가능하게 합니다.x-forwarded-client-cert: "...URI=spiffe://cluster.local/ns/part1-demo/sa/default": 우리는 HTTP 평문으로 요청을 보냈는데도, 두 파드의 Envoy 사이드카끼리는 자동으로 상호 암호화(mTLS) 터널을 뚫고 서로의 SPIFFE 전자 신분증을 확인했습니다!
실습 5. Envoy 내부 메모리 해부: xDS 5단계 직접 열어보기
강의 노트 5절에서 배운 Envoy의 5대 동적 설정(LDS → RDS → CDS → EDS + SDS) 이 실제로 study-client 파드 옆의 Envoy 메모리에 어떻게 들어 있는지 istioctl 명령어로 하나씩 직접 열어보겠습니다.
5-1. 전체 동기화 상태 확인 (proxy-status)
먼저 통제 본부(istiod)와 현장의 파드 3개(Envoy) 사이에 직통 전화가 잘 연결되어 있는지 전체 현황판을 봅니다.
istioctl proxy-status
출력 예시:
NAME CLUSTER CDS LDS EDS RDS ECDS ISTIOD VERSION
istio-ingressgateway-5b57c8df78-w27tk.istio-system Kubernetes SYNCED (1m) SYNCED (1m) SYNCED (1m) NOT SENT NOT SENT istiod-6f5989b555-tkf9q 1.31.3
order-api-d757f5dc9-fpz54.part1-demo Kubernetes SYNCED (45s) SYNCED (45s) SYNCED (45s) SYNCED (45s) NOT SENT istiod-6f5989b555-tkf9q 1.31.3
order-api-d757f5dc9-hsthv.part1-demo Kubernetes SYNCED (43s) SYNCED (43s) SYNCED (43s) SYNCED (43s) NOT SENT istiod-6f5989b555-tkf9q 1.31.3
study-client.part1-demo Kubernetes SYNCED (35s) SYNCED (35s) SYNCED (35s) SYNCED (35s) NOT SENT istiod-6f5989b555-tkf9q 1.31.3
CDS,LDS,EDS,RDS칸이 모두SYNCED(동기화 완료) 로 표시되어 있습니다!istiod본부가 준 최신 지도가 모든 Envoy 요원의 메모리에 100% 똑같이 들어 있다는 뜻입니다.
5-2. 1단계 LDS (Listener — 접수 창구) 확인
study-client 파드의 Envoy가 몇 번 포트(창구)를 열고 패킷을 기다리고 있는지 확인합니다.
istioctl proxy-config listener study-client.part1-demo
출력 예시 (핵심 줄 발췌):
ADDRESSES PORT MATCH DESTINATION
10.96.0.10 53 ALL Cluster: outbound|53||kube-dns.kube-system.svc.cluster.local
0.0.0.0 80 Trans: raw_buffer; App: http/1.1,h2c Route: 80
...
0.0.0.0 15001 ALL PassthroughCluster
0.0.0.0 15006 Trans: tls; App: istio-http/1.0,istio-http/1.1,istio-h2 InboundPassthroughCluster
0.0.0.0 15006 Trans: raw_buffer; App: http/1.1,h2c InboundPassthroughCluster
0.0.0.0 15090 ALL Inline Route: /stats/prometheus*
LDS)
0.0.0.0:15001(virtualOutbound): 내 파드(study-client) 안에서 밖으로 나가는 모든 패킷이 맨 처음 끌려 들어오는 출구 총괄 창구입니다(2주차에 왜 15001번으로 끌려오는지 커널iptables를 뜯어봅니다).0.0.0.0:15006(virtualInbound): 바깥에서 내 파드로 들어오는 모든 패킷이 맨 처음 끌려 들어오는 입구 총괄 창구입니다.0.0.0.0:80→Route: 80: 15001번 창구로 들어온 패킷 중 목적지 포트가 80번(http://order-api:80) 이고 프로토콜이 HTTP(http/1.1, h2c)인 요청은80번 안내 이정표(Route: 80)를 보라! 고 연결해 줍니다.
5-3. 2단계 RDS (Route — 안내 이정표) 확인
그럼 방금 LDS가 가리킨 80번 이정표(--name 80) 에 무엇이 적혀 있는지 열어봅니다.
istioctl proxy-config route study-client.part1-demo --name 80
출력 예시:
NAME VHOST NAME DOMAINS MATCH VIRTUAL SERVICE
80 order-api.part1-demo.svc.cluster.local:80 order-api, order-api.part1-demo + 1 more... /*
RDS)
DOMAINS: 요청의Host헤더가 짧은 이름인order-api이든,order-api.part1-demo이든, 전체 주소인order-api.part1-demo.svc.cluster.local이든 모두 똑같이 인식합니다!VHOST NAME: 이 도메인으로 온 모든 경로(/*) 요청은order-api.part1-demo.svc.cluster.local:80목적지 부서(Cluster)로 넘기라고 적혀 있습니다.
5-4. 3단계 CDS (Cluster — 목적지 부서 명부와 배달 규칙) 확인
이제 Envoy의 목적지 부서 명부(CDS) 중에서 order-api 부서를 찾아봅니다.
istioctl proxy-config cluster study-client.part1-demo --fqdn order-api.part1-demo.svc.cluster.local
출력 예시:
SERVICE FQDN PORT SUBSET DIRECTION TYPE DESTINATION RULE
order-api.part1-demo.svc.cluster.local 80 - outbound EDS
이 부서에 어떤 배달 규칙(로드밸런싱 알고리즘)이 걸려 있는지 -o json 옵션으로 살짝 들여다볼까요?
istioctl proxy-config cluster study-client.part1-demo --fqdn order-api.part1-demo.svc.cluster.local -o json | Select-String '"name"|lbPolicy'
출력 예시:
"name": "outbound|80||order-api.part1-demo.svc.cluster.local",
"lbPolicy": "LEAST_REQUEST",
CDS)
TYPE: EDS: "이 부서에 속한 실제 서버들의 IP 주소는 고정된 게 아니라EDS(4단계 주소록)에서 실시간으로 받아오겠다!"는 뜻입니다.lbPolicy: LEAST_REQUEST(또는ROUND_ROBIN): 최신 Istio는 한 걸음 더 나아가, 단순히 기계적으로 번갈아 보내는 것을 넘어 현재 처리 중인 요청이 가장 적은(가장 한가한) 파드에게 우선 배정해 주는 똑똑한 L7 분배 정책을 기본으로 사용합니다(파드 2개가 똑같이 한가할 때는 1:1 교차 분배로 동작합니다).
5-5. 4단계 EDS (Endpoint — 실제 살아 있는 파드들의 진짜 IP 주소록) 확인
드디어 최종 목적지입니다! outbound|80||order-api.part1-demo.svc.cluster.local 부서에 지금 실제로 출근해서 일하고 있는 파드들의 진짜 IP(EDS)를 열어봅니다.
istioctl proxy-config endpoint study-client.part1-demo --cluster "outbound|80||order-api.part1-demo.svc.cluster.local"
출력 예시:
ENDPOINT STATUS OUTLIER CHECK CLUSTER
10.244.1.4:8080 HEALTHY OK outbound|80||order-api.part1-demo.svc.cluster.local
10.244.3.5:8080 HEALTHY OK outbound|80||order-api.part1-demo.svc.cluster.local
10.244.1.4, 10.244.3.5)와 100% 일치합니다!
ENDPOINT: 쿠버네티스Service의 대표 IP(10.96.236.120:80)가 아니라, 실제 파드 2개의 진짜 IP와 컨테이너 포트(10.244.1.4:8080,10.244.3.5:8080) 가 직접 들어 있습니다! 그래서 Envoy가kube-proxy를 거치지 않고 파드로 직접 패킷을 쏠 수 있는 것입니다.STATUS: HEALTHY/OUTLIER CHECK: OK: 두 파드 모두 건강하며, 서킷 브레이커(이상 탐지) 검사도 통과(OK)했다는 뜻입니다(5주차에 일부러 에러를 내서 이 칸이UNHEALTHY로 바뀌며 격리되는 장면을 실습합니다!).
5-6. 5단계 SDS (Secret — SPIFFE 전자 신분증) 확인
마지막으로, istiod 본부가 study-client 파드의 Envoy에게 발급해 준 전자 신분증(X.509 인증서)을 확인합니다.
istioctl proxy-config secret study-client.part1-demo
출력 예시:
RESOURCE NAME TYPE STATUS VALID CERT SERIAL NUMBER NOT AFTER NOT BEFORE
default Cert Chain ACTIVE true 829104b27f18c9e04129b8a172849102 2026-10-26T06:20:00Z 2026-10-25T06:18:00Z
ROOTCA CA ACTIVE true 64019283746501928374650192837465 2036-10-22T06:18:00Z 2026-10-25T06:18:00Z
default(VALID CERT: true): 이 파드의 신분증입니다.NOT BEFORE와NOT AFTER를 보면 유효기간이 딱 24시간(하루) 짜리인 단기 신분증입니다! 혹시 털리더라도 하루면 만료되고,istiod가 만료 전에SDS통로로 새 신분증을 조용히 갈아 끼워 줍니다(4주차에 이 인증서 내부를openssl로 해부합니다).
실습 6. 상태 유지 및 정리
part1-demo 파드들은 지우지 마세요!
곧바로 이어지는 2주차 실습(1세대 Sidecar 메시의 커널 내부 해부) 에서 지금 떠 있는 study-client와 order-api 파드 내부의 리눅스 커널(iptables -t nat, 15006/15001 포트, UID 1337)을 그대로 이어서 해부합니다!
현재 상태가 아래처럼 2/2 Running 이면 완벽합니다.
kubectl get pods -n part1-demo
NAME READY STATUS RESTARTS AGE
order-api-d757f5dc9-fpz54 2/2 Running 0 10m
order-api-d757f5dc9-hsthv 2/2 Running 0 10m
study-client 2/2 Running 0 10m
자주 나는 문제와 해결
| 증상 | 원인 | 해결 방법 |
|---|---|---|
istioctl: The term 'istioctl' is not recognized |
현재 PowerShell 창에 $HOME\bin 경로가 안 잡혀 있음 |
$env:Path += ";$HOME\bin" 실행 (새 PowerShell 창을 열면 자동으로 잡힘) |
네임스페이스에 istio-injection=enabled 라벨을 붙였는데 파드가 여전히 1/1 임 |
라벨을 붙이기 전에 만들어진 파드라서 사이드카가 안 들어감 | kubectl rollout restart deployment/order-api -n part1-demo 로 파드를 다시 생성 |
kubectl exec -n part1-demo study-client -- curl ... 실행 시 컨테이너 선택 경고 출력 |
파드 안에 컨테이너가 2개(study-client, istio-proxy)여서 어느 컨테이너인지 모호함 |
-c study-client 옵션을 명시 (kubectl exec -n part1-demo study-client -c study-client -- ...) |
curl 명령을 그냥 PowerShell 에서 칠 때 에러 발생 |
PowerShell 에서 curl 은 Invoke-WebRequest 의 별칭(Alias)임 |
호스트 PC에서 직접 칠 때는 curl.exe 로 입력 (kubectl exec 안에서는 리눅스 파드 내부의 진짜 curl 이 실행되므로 curl 그대로 사용 가능) |
핵심 명령어 정리
| 명령어 | 하는 일 |
|---|---|
istioctl install --set profile=default -y |
Istio 컨트롤 플레인(istiod)과 기본 구성을 클러스터에 설치 |
kubectl label ns <이름> istio-injection=enabled |
해당 네임스페이스에 새로 뜨는 파드에 Envoy 사이드카 자동 주입 활성화 |
kubectl rollout restart deployment/<이름> -n <NS> |
디플로이먼트의 파드들을 무중단으로 다시 만들어 사이드카 주입 적용 |
istioctl proxy-status |
클러스터 내 모든 Envoy 프록시와 istiod 간의 xDS 동기화(SYNCED) 상태 확인 |
istioctl proxy-config listener <파드.NS> |
Envoy의 LDS (열려 있는 리스너 포트 15001, 15006, 80 등) 확인 |
istioctl proxy-config route <파드.NS> --name 80 |
Envoy의 RDS (도메인·경로별 라우팅 이정표) 확인 |
istioctl proxy-config cluster <파드.NS> --fqdn <주소> |
Envoy의 CDS (목적지 서비스 그룹 및 로드밸런싱 정책) 확인 |
istioctl proxy-config endpoint <파드.NS> --cluster "<이름>" |
Envoy의 EDS (실제 살아있는 목적지 파드들의 진짜 IP:포트 목록) 확인 |
istioctl proxy-config secret <파드.NS> |
Envoy의 SDS (메모리에 발급된 SPIFFE X.509 인증서 상태) 확인 |
셀프 점검
핵심 질문 7가지
- 클러스터 외부 고객이 정문(Gateway)으로 들어오는 트래픽과, 클러스터 내부 파드들끼리 서로 호출하는 트래픽을 각각 무슨 트래픽이라고 부르나요?
- 파드의
READY상태가1/1에서2/2로 바뀌었을 때, 새로 추가된 두 번째 컨테이너의 이름과 역할은 무엇인가요? - 쿠버네티스 기본
kube-proxy(L4) 환경에서 HTTP Keep-Alive나 gRPC를 쓰면 왜 한쪽 파드로만 요청이 전부 몰리나요? - 만약
istio-system네임스페이스의istiod파드가 갑자기 죽으면, 현재 통신 중인study-client→order-api간의 요청도 끊기나요? 그 이유는? - Envoy의 5대
xDS설정(LDS,RDS,CDS,EDS,SDS)은 각각 무엇의 약자이며 무슨 역할을 하나요? - Envoy가
order-api로 패킷을 보낼 때 쿠버네티스Service의 대표 IP(10.96.x.x)로 보내지 않고EDS에 적힌 파드 IP(10.244.x.x)로 직접 보내는 이유는 무엇인가요? - Envoy 클러스터 이름
outbound|80||order-api.part1-demo.svc.cluster.local에서 파이프(|)로 구분된 4칸은 각각 무엇을 뜻하나요?
- 외부 ↔ 내부 출입은 남북 트래픽(North-South Traffic), 내부 파드 ↔ 옆 파드 간 통신은 동서 트래픽(East-West Traffic) 이라고 부릅니다.
- 컨테이너 이름은
istio-proxy(실제 엔진은 Envoy 프록시)이며, 내 앱 컨테이너 옆에 붙어 들어오고 나가는 모든 네트워크 통신·L7 로드밸런싱·mTLS 암호화·메트릭 수집을 대신 처리하는 사이드카(Sidecar) 프록시입니다. kube-proxy는 L4(TCP 계층) 만 보기 때문에 맨 처음 TCP 연결을 맺는 순간(SYN패킷) 에만 목적지 파드를 고르고 커널conntrack장부에 기록합니다. Keep-Alive나 gRPC는 처음 맺은 1개의 TCP 연결을 끊지 않고 계속 재사용하므로, 이후의 모든 요청이 처음 선택된 파드 한 곳으로만 고착화(Connection Pinning)됩니다.- 끊기지 않습니다. 실제 요청 패킷은 데이터 플레인(Envoy 사이드카들)끼리만 직접 주고받으며 컨트롤 플레인(
istiod)을 거치지 않습니다. 각 Envoy는 이미 메모리에 받아 둔xDS설정을 가지고 정상 통신을 계속합니다. LDS(Listener): 접수 창구 포트 열기(15001,15006등) /RDS(Route): 도메인·경로 이정표 매칭 /CDS(Cluster): 목적지 서비스 부서 및 로드밸런싱 규칙 정의 /EDS(Endpoint): 실제 살아있는 파드들의 진짜 IP 목록 /SDS(Secret): mTLS용 SPIFFE X.509 전자 신분증 발급·갱신.- 만약
Service의 대표 IP(ClusterIP)로 보내면 노드의kube-proxy(L4)가 다시 개입하여 연결 고착화가 발생할 수 있기 때문에, Envoy는kube-proxy를 우회하고EDS로 파악한 실제 파드 IP로 직접 L7 로드밸런싱을 수행합니다. outbound(밖으로 나가는 방향) |80(서비스 포트) |(빈칸)(서브셋/버전 이름) |order-api.part1-demo.svc.cluster.local(목적지 서비스의 전체 도메인 이름, FQDN).
추가 실험: 파드 스케일아웃(2개 → 4개) 시 EDS 실시간 반영 확인
기본 kube-proxy는 오토스케일링으로 파드를 늘려도 기존 Keep-Alive 연결이 새 파드로 가지 않지만, Envoy 사이드카가 붙은 상태에서 파드를 2개에서 4개로 늘리면 어떻게 되는지 확인합니다.
# 1. order-api 파드를 2개 → 4개로 즉시 확장!
kubectl scale deployment order-api -n part1-demo --replicas=4
kubectl rollout status deployment/order-api -n part1-demo
# 2. study-client 의 Envoy 메모리(EDS)에 파드 IP 4개가 즉시 들어왔는지 확인!
istioctl proxy-config endpoint study-client.part1-demo --cluster "outbound|80||order-api.part1-demo.svc.cluster.local"
# 3. Keep-Alive 연결 하나로 4번 연속 호출했을 때 4개 파드로 골고루 나뉘는지 확인!
kubectl exec -n part1-demo study-client -c study-client -- curl -s --http1.1 "http://order-api/test?[1-4]" | Select-String '"hostname"'
실측 결과: study-client를 재시작하지도 않았는데 1초 만에 EDS 목록이 4줄(10.244.1.4, 10.244.1.5, 10.244.3.5, 10.244.3.7)로 늘어나고, Keep-Alive 요청도 새로 뜬 파드들까지 즉시 골고루 나뉘어 들어갑니다.
확인이 끝나면 Part 02 실습을 위해 다시 파드 개수를 2개로 돌려놓습니다.
kubectl scale deployment order-api -n part1-demo --replicas=2
order-api.yaml 파일을 한 글자도 고치지 않았는데 파드가 2/2가 되었고, 내 앱은 평소처럼 80번 포트로 요청을 던졌는데 어느새 Envoy의 15001번 창구로 패킷이 끌려 들어갔습니다.
"도대체 누가 내 YAML을 몰래 고쳐서 컨테이너를 끼워 넣었으며(MutatingAdmissionWebhook), 리눅스 커널 안에서는 무슨 마법(istio-init, iptables NAT 테이블, UID 1337 무한 루프 방지)이 벌어지고 있는 걸까요?"
Part 02에서 파드의 리눅스 커널 네트워크 네임스페이스 안으로 직접 들어가 그 비밀을 파헤칩니다.
네비게이션
| 이전 | 목록 | 다음 |
|---|---|---|
| ◀ Part 01 이론 | 목록으로 | Part 02 이론 ▶ |